package com.gitee.wsl.text.dictomaton.levenshtein

import com.gitee.wsl.flowing.pipeline.PipelinePhaseRelation.Before
import com.gitee.wsl.struct.compare.similarity.Levenshtein
import com.gitee.wsl.text.dictomaton.DictionaryBuilder
import com.gitee.wsl.text.dictomaton.DictionaryBuilderException
import kotlin.random.Random
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals


class LevenshteinAutomatonTest {
    private val d_characters = charArrayOf('a', 'b', 'c', 'd', 'e', 'f')
    private val d_editOperations = arrayOf<RandomEditOperation>(RandomSubstitution(), RanndomInsert(), RandomDelete())

    private val MIN_LENGTH = 5
    private val MAX_LENGTH = 15
    private val N_PERMUTED_STRINGS = 1000
    private val N_REPETITIONS = 500

    private lateinit var d_rng: Random

    @BeforeTest
    fun initialize() {
        d_rng = Random(42)
    }

    @Test
    @Throws(DictionaryBuilderException::class)
    fun intersectionLanguage1Test() {
        for (attempt in 0..<N_REPETITIONS) generateAndCheckPermutations(
            MIN_LENGTH,
            MAX_LENGTH,
            N_PERMUTED_STRINGS,
            3,
            1
        )
    }

    @Test
    @Throws(DictionaryBuilderException::class)
    fun intersectionLanguage2Test() {
        for (attempt in 0..<N_REPETITIONS) generateAndCheckPermutations(
            MIN_LENGTH,
            MAX_LENGTH,
            N_PERMUTED_STRINGS,
            4,
            2
        )
    }

    /**
     * Generate a word, create a dictionary of permutations permutations that are created using random edit operations,
     * and check that Levenshtein automaton for that word finds permutations within its edit distance.
     *
     * @param minLength             The minimum lenth of the generated word.
     * @param maxLength             The maximum length of the generated word.
     * @param nPermutations         The number of permutations to generate.
     * @param nRandomEditOperations The maximum number of random edit operations.
     * @param distance              Test the levenshtein automaton with this edit distance.
     * @throws DictionaryBuilderException
     */
    @Throws(DictionaryBuilderException::class)
    private fun generateAndCheckPermutations(
        minLength: Int,
        maxLength: Int,
        nPermutations: Int, 
        nRandomEditOperations: Int,
        distance: Int,
    ) {
        val str = randomString(minLength + (maxLength - minLength + 1))

        val all = mutableSetOf<String>()
        val shouldHave = HashSet<String>()

        for (i in 0..<nPermutations) {
            val n: Int = d_rng.nextInt(nRandomEditOperations)

            val permutedBuilder= StringBuilder(str)
            for (perm in 0..<n) d_editOperations[d_rng.nextInt(d_editOperations.size)].apply(permutedBuilder)

            val permuted = permutedBuilder.toString()

            all.add(permuted)

            if (Levenshtein.distance(str, permuted) <= distance) shouldHave.add(permuted)
        }

        val dict = DictionaryBuilder().addAll(all).build()
        val la = LevenshteinAutomaton(str, distance)

        assertEquals(shouldHave, la.intersectionLanguage(dict))
    }

    private interface RandomEditOperation {
        fun apply(string: StringBuilder)
    }

    private inner class RandomSubstitution : RandomEditOperation {
        override fun apply(sb: StringBuilder) {
            sb.set(d_rng.nextInt(sb.length), d_characters[d_rng.nextInt(d_characters.size)])
        }
    }

    private inner class RandomDelete : RandomEditOperation {
        override fun apply(sb: StringBuilder) {
            sb.deleteAt(d_rng.nextInt(sb.length))
        }
    }

    private inner class RanndomInsert : RandomEditOperation {
        override fun apply(sb: StringBuilder) {
            sb.insert(
                d_rng.nextInt(sb.length + 1),
                d_characters[d_rng.nextInt(d_characters.size)]
            )
        }
    }

    private fun randomString(length: Int): String {
        val string: StringBuilder = StringBuilder()

        for (i in 0..<length) string.append(d_characters[d_rng.nextInt(d_characters.size)])

        return string.toString()
    }
}
